/*
 * This file is part of the OSMembrane project.
 * More informations under www.osmembrane.de
 * 
 * The project is licensed under the GNU GENERAL PUBLIC LICENSE 3.0.
 * for more details about the license see http://www.osmembrane.de/license/
 * 
 * Source: $HeadURL: http://osmembrane-gui.googlecode.com/svn/sources/src/de/osmembrane/view/panels/PipelinePanel.java $ ($Revision: 902 $)
 * Last changed: $Date: 2011-03-09 17:41:55 +0000 (Wed, 09 Mar 2011) $
 */


package de.osmembrane.view.panels;

import java.awt.Adjustable;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.GridLayout;
import java.awt.Point;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Observable;
import java.util.Observer;

import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.JLayeredPane;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollBar;

import de.osmembrane.Application;
import de.osmembrane.controller.ActionRegistry;
import de.osmembrane.controller.actions.AddConnectionAction;
import de.osmembrane.controller.actions.AddFunctionAction;
import de.osmembrane.controller.actions.DeleteSelectionAction;
import de.osmembrane.controller.actions.DuplicateFunctionAction;
import de.osmembrane.controller.actions.MoveFunctionAction;
import de.osmembrane.controller.events.ConnectingFunctionsEvent;
import de.osmembrane.controller.events.ContainingLocationEvent;
import de.osmembrane.exceptions.ControlledException;
import de.osmembrane.exceptions.ExceptionSeverity;
import de.osmembrane.model.ModelProxy;
import de.osmembrane.model.pipeline.AbstractConnector;
import de.osmembrane.model.pipeline.AbstractFunction;
import de.osmembrane.model.pipeline.Connector;
import de.osmembrane.model.pipeline.PipelineObserverObject;
import de.osmembrane.model.pipeline.PipelineObserverObject.ChangeType;
import de.osmembrane.model.settings.SettingType;
import de.osmembrane.tools.I18N;
import de.osmembrane.view.ViewRegistry;
import de.osmembrane.view.components.JSilentScrollBar;
import de.osmembrane.view.interfaces.IZoomDevice;

/**
 * This is the pipeline view, i.e. the panel that shows the entire pipeline with
 * all {@link PipelineFunction}s, {@link PipelineConnector}s, and
 * {@link PipelineLink}s.
 * 
 * <b>Note</b>: In order to work with all the fancy zoom and move stuff, the
 * {@link PipelinePanel} defines 2 specific coordinate systems:
 * <ol>
 * <li>The window space - int window coordinates as defined in Swing.</li>
 * <li>The object space - double coordinates that are stored in the model.</li>
 * </ol>
 * Therefore it might be a good idea to always specify when coordinates or
 * points occur whether they are treated as object and window space.
 * 
 * The object space is defined as follows: a length of 1 window coordinate = a
 * length of 1.0 object coordinate in the initial view, window axes = object
 * axes.
 * 
 * In order to produce the viewable output you have to apply
 * {@link AffineTransform}ations from the object space to the window space. In
 * order to translate locations from the window to the object space, you have to
 * do the same vice versa.
 * 
 * There should never be a direct access to perform the transformations. Various
 * winToObj and objToWin transforming functions are provided for this matter.
 * 
 * Additionally, currently happening transformation changes should only change
 * the currentDisplay transformation and, when final, "pre-apply" it (multiply
 * from the left side) to the objectToWindow transformation.
 * 
 * @see "Spezifikation.pdf, chapter 2.1.5 (German)"
 * 
 * @see "your Math reference book, sections 'Linear Algebra', 'Matrices',
 *      'Linear Transformations'"
 * 
 * @author tobias_kuhn
 * 
 */
public class PipelinePanel extends JPanel implements Observer, IZoomDevice {

	private static final long serialVersionUID = 2544369818627179591L;

	/**
	 * list of {@link PipelineFunction} currently being present on the panel
	 */
	private List<PipelineFunction> functions;

	/**
	 * map of {@link PipelineConnector} for the {@link Connector}s in the model,
	 * which currently being present on the panel
	 */
	private Map<AbstractConnector, PipelineConnector> connectors;

	/**
	 * The transformation which transforms the object coordinates to window
	 * coordinates, depending on the view port and the zooming level
	 */
	private AffineTransform objectToWindow;

	/**
	 * A temporary transformation to be used after objectToWindow to represent
	 * the current changes of the display (move dragging, zoom animation)
	 */
	private AffineTransform currentDisplay;

	/**
	 * The layered pane to show {@link PipelineFunction}s,
	 * {@link PipelineConnector}s, {@link PipelineLink}s (in the particular
	 * order defined by FUNCTION_LAYER, CONNECTOR_LAYER and LINK_LAYER)
	 */
	private JLayeredPane layeredPane;

	/**
	 * The constants applicable for the layeredPane.
	 */
	private static final Integer FUNCTION_LAYER = new Integer(3);
	private static final Integer CONNECTOR_LAYER = new Integer(2);
	private static final Integer LINK_LAYER = new Integer(1);

	/**
	 * Saves the point (in object coordinates) when a drag and drop action
	 * occurs *inside* the {@link PipelinePanel} (i.e. not from the
	 * {@link LibraryPanel})
	 */
	private Point2D draggingFrom;

	/**
	 * The standard zoom values.
	 */
	private final static double DEFAULT_ZOOM_IN = 1.25;
	private final static double DEFAULT_ZOOM_OUT = 0.80;
	protected static final double DEFAULT_ZOOM = 1.0;
	private final static double PIXEL_PER_ZOOM_LEVEL = 100.00;

	/**
	 * The links to the {@link InspectorPanel} used for communication between
	 * these two components.
	 */
	private InspectorPanel functionInspector;

	/**
	 * The vertical and horizontal {@link JScrollBar} that can be used for
	 * moving the view.
	 */
	private JSilentScrollBar verticalScroll;
	private JSilentScrollBar horizontalScroll;

	/**
	 * The upper-left-most and the bottom-right-most point on the whole pipeline
	 * in object space.
	 */
	private Point2D objTopLeft;
	private Point2D objBottomRight;

	/**
	 * The currently selected {@link Tool}.
	 */
	private Tool activeTool;

	/**
	 * The currently selected object (either a {@link PipelineFunction} or a
	 * {@link PipelineLink})
	 */
	private Object selected;

	/**
	 * The temporary saving slot for creating a two point {@link PipelineLink}.
	 */
	private PipelineFunction connectionStart;

	/**
	 * The temporary link to show the creation of a new connection
	 */
	private PipelinePreviewLink connectionPreview;

	/**
	 * Initializes a new {@link PipelinePanel}
	 * 
	 * @param functionInspector
	 *            the {@link InspectorPanel} to handle the edits for the
	 *            selected objects
	 * @param popup
	 *            the Popup to be displayed on right clicks
	 */
	public PipelinePanel(InspectorPanel functionInspector,
			final JPopupMenu popup) {

		setLayout(new GridLayout(1, 1));

		// internal values
		this.functions = new ArrayList<PipelineFunction>();
		this.connectors = new HashMap<AbstractConnector, PipelineConnector>();
		this.functionInspector = functionInspector;

		this.verticalScroll = new JSilentScrollBar(Adjustable.VERTICAL, 0, 0,
				0, 0);
		this.horizontalScroll = new JSilentScrollBar(Adjustable.HORIZONTAL, 0,
				0, 0, 0);
		AdjustmentListener al = new AdjustmentListener() {

			@Override
			public void adjustmentValueChanged(AdjustmentEvent e) {
				if (e.getSource() == verticalScroll) {
					moveTopTo(e.getValue());
					arrange(false);
				} else if (e.getSource() == horizontalScroll) {
					moveLeftTo(e.getValue());
					arrange(false);
				}
			}
		};
		this.verticalScroll.addAdjustmentListener(al);
		this.horizontalScroll.addAdjustmentListener(al);

		this.objTopLeft = new Point2D.Double();
		this.objBottomRight = new Point2D.Double();

		this.activeTool = Tool.DEFAULT_MAGIC_TOOL;
		this.selected = null;
		this.connectionStart = null;
		this.connectionPreview = new PipelinePreviewLink(this);

		this.objectToWindow = new AffineTransform();
		double zoomFactor = PipelinePanel.DEFAULT_ZOOM
				* (Double) ModelProxy.getInstance().getSettings()
						.getValue(SettingType.DEFAULT_ZOOM_SIZE);
		this.objectToWindow.setToScale(zoomFactor, zoomFactor);
		this.currentDisplay = new AffineTransform();

		this.layeredPane = new JLayeredPane();
		this.layeredPane.setVisible(true);
		this.layeredPane.setOpaque(true);
		add(this.layeredPane);

		this.layeredPane.add(this.connectionPreview);

		// register as observer
		ViewRegistry.getInstance().addObserver(this);

		// all listeners for all kind of events

		addMouseWheelListener(new MouseWheelListener() {
			@Override
			public void mouseWheelMoved(MouseWheelEvent e) {
				// zoom with mouse wheel
				if (e.getWheelRotation() < 0) {
					zoom(e.getPoint(), DEFAULT_ZOOM_IN);
				} else {
					zoom(e.getPoint(), DEFAULT_ZOOM_OUT);
				}
			}
		});

		addMouseListener(new MouseListener() {

			@Override
			public void mouseReleased(MouseEvent e) {
				// popup on right click
				if (e.getButton() == MouseEvent.BUTTON3) {
					popup.show(e.getComponent(), e.getX(), e.getY());
				}

				// view tool and magic
				switch (activeTool) {
				case DEFAULT_MAGIC_TOOL:
					if (selected != null) {
						break;
					}
				case VIEW_TOOL:
					if (e.isControlDown()) {
						// zoom
						objectToWindow.preConcatenate(currentDisplay);
						currentDisplay.setToIdentity();
					} else {
						// move
						objectToWindow.preConcatenate(currentDisplay);
						currentDisplay.setToIdentity();
					}
					arrange(true);
					break;
				}

				// select tool and magic
				switch (activeTool) {
				case DEFAULT_MAGIC_TOOL:
				case SELECTION_TOOL:
					if ((selected != null)
							&& (selected instanceof PipelineFunction)
							&& (draggingFrom != null)) {
						PipelineFunction pf = (PipelineFunction) selected;

						// getCoordinate - draggingFrom = offset to add
						Point newWinPos = e.getPoint();
						Point2D objOffset = new Point2D.Double(pf
								.getModelLocation().getX()
								- draggingFrom.getX(), pf.getModelLocation()
								.getY() - draggingFrom.getY());

						Point winOffset = objToWindowDelta(objOffset);
						newWinPos.translate(winOffset.x, winOffset.y);

						newWinPos = findNextFreePoint(newWinPos, pf);

						Point2D newObjPosition = windowToObj(newWinPos);

						// require a minimum distance to drag & drop
						if (newObjPosition.distance(pf.getModelLocation()) < PipelineFunction.PIPELINE_FUNCTION_MIN_DRAG_DISTANCE) {
							pf.setLocation(objToWindow(pf.getModelLocation()));
							pf.arrangeConnectors();
							pf.arrangeLinks();
							// return, so we don't create an undo step
							return;
						}

						// set position
						Action a = ActionRegistry.getInstance().get(
								MoveFunctionAction.class);
						ContainingLocationEvent cle = new ContainingLocationEvent(
								this, pf.getModelFunction(), newObjPosition);
						a.actionPerformed(cle);
					}
				}

				// connection tool and magic
				switch (activeTool) {
				case DEFAULT_MAGIC_TOOL:
				case SELECTION_TOOL:
					abortConnect();
				}

				draggingFrom = null;
			}

			@Override
			public void mousePressed(MouseEvent e) {
				switch (activeTool) {
				case SELECTION_TOOL:
					selected(null);
					repaint();
					break;
				case DEFAULT_MAGIC_TOOL:
				case VIEW_TOOL:
					selected(null);
					// start dragging
					draggingFrom = windowToObj(e.getPoint());
					currentDisplay.setToIdentity();
					break;
				}
			}

			@Override
			public void mouseExited(MouseEvent e) {
			}

			@Override
			public void mouseEntered(MouseEvent e) {
			}

			@Override
			public void mouseClicked(MouseEvent e) {
			}
		});

		addMouseMotionListener(new MouseMotionListener() {

			@Override
			public void mouseMoved(MouseEvent e) {
				// when the start of new connection is determined, rearrange it
				if (connectionStart != null) {
					connectionPreview.setTarget(e.getPoint());
					connectionPreview.regenerateLine();
					repaint();
				}
				PipelinePanel.this.connectionPreview
						.setVisible(PipelinePanel.this.connectionStart != null);
			}

			@Override
			public void mouseDragged(MouseEvent e) {

				// view tool and magic
				switch (activeTool) {
				case DEFAULT_MAGIC_TOOL:
					if (selected != null) {
						break;
					}
				case VIEW_TOOL:
					if (draggingFrom != null) {
						Point2D draggingTo = windowToObjFixed(e.getPoint());

						if (e.isControlDown()) {
							// zoom
							double winDist = objToWindowFixed(draggingFrom)
									.distance(e.getPoint())
									* Math.signum(draggingFrom.getY()
											- draggingTo.getY());
							zoomTemp(
									draggingFrom,
									Math.exp(winDist * Math.log(2.0)
											/ PIXEL_PER_ZOOM_LEVEL));
						} else {
							// move
							currentDisplay.setToTranslation(
									objectToWindow.getScaleX()
											* (draggingTo.getX() - draggingFrom
													.getX()),
									objectToWindow.getScaleY()
											* (draggingTo.getY() - draggingFrom
													.getY()));
							arrange(true);
						}
					}
					break;
				}

				// selection tool and magic
				switch (activeTool) {
				case DEFAULT_MAGIC_TOOL:
				case SELECTION_TOOL:
					if ((selected != null)
							&& (selected instanceof PipelineFunction)
							&& (draggingFrom != null)) {
						PipelineFunction pf = (PipelineFunction) selected;

						// getCoordinate - draggingFrom
						Point2D objOffset = new Point2D.Double(pf
								.getModelLocation().getX()
								- draggingFrom.getX(), pf.getModelLocation()
								.getY() - draggingFrom.getY());
						Point winOffset = objToWindowDelta(objOffset);

						// translate
						e.translatePoint(winOffset.x, winOffset.y);
						pf.setLocation(e.getPoint());

						pf.arrangeConnectors();
						pf.arrangeLinks();

						// now arrange all *incoming* links, since connectors
						// only contain their outgoing connections
						for (PipelineConnector pfCon : pf.getConnectors()) {
							if (!pfCon.isOutpipes()) {
								for (PipelineLink pfInLink : pfCon.getInLinks()) {
									pfInLink.getLinkSource().arrangeLinks();
								}
							}
						}

					} /* if something selected */
				}

			} /* mouseDragged */
		});

		// necessary to initialize with correct model reflection
		update(null, new PipelineObserverObject(ChangeType.FULLCHANGE, null));
	}

	/**
	 * Moves objectToWindow in a way, so that the left screen (window x = 0)
	 * will result in the object coordinate to.
	 * 
	 * @param to
	 *            the object position to move the left window edge to
	 */
	private void moveLeftTo(double to) {
		// if you can't read the Math below properly,
		// some idiot used Eclipse/Java auto code formatter
		/*
		 * o2w.scale * to + o2w.translate = 0 <=> o2w.translate = - o2w.scale *
		 * to
		 */
		double translateToX = -objectToWindow.getScaleX() * to;

		/*
		 * o2w * M = o2w_new, where o2w is of the form scale(x,y) and
		 * translate(s,t) and M is a translation matrix from translate(u,v) and
		 * o2w_new.translate is of the form scale(x,y) and
		 * translateTo(translateToX,t) <=> u * x + s = translateToX v * y + t =
		 * t <=> u = (translateToX - s) / x v = 0
		 */
		objectToWindow.translate(
				(translateToX - objectToWindow.getTranslateX())
						/ (objectToWindow.getScaleX()), 0.0);
	}

	/**
	 * Moves objectToWindow in a way, so that the top screen (window y = 0) will
	 * result in the object coordinate to.
	 * 
	 * @param to
	 *            the object position to move the top window edge to
	 */
	private void moveTopTo(double to) {
		// if you can't read the Math below properly,
		// some idiot used Eclipse/Java auto code formatter
		/*
		 * o2w.scale * to + o2w.translate = 0 <=> o2w.translate = - o2w.scale *
		 * to
		 */
		double translateToY = -objectToWindow.getScaleY() * to;

		/*
		 * o2w * M = o2w_new, where o2w is of the form scale(x,y) and
		 * translate(s,t) and M is a translation matrix from translate(u,v) and
		 * o2w_new.translate is of the form scale(x,y) and
		 * translateTo(s,translateToY) <=> u * x + s = s v * y + t =
		 * translateToY <=> u = 0 v = (translateToY - t) / y
		 */
		objectToWindow.translate(0.0,
				(translateToY - objectToWindow.getTranslateY())
						/ (objectToWindow.getScaleY()));
	}

	/**
	 * Fully translates window coordinates to object coordinates.
	 * 
	 * @param window
	 *            window coordinates
	 * @return window in object coordinates, null if there is an error with the
	 *         transformations which should theoretically never be the case
	 */
	protected Point2D windowToObj(Point window) {
		Point2D result = new Point2D.Double();

		try {
			currentDisplay.inverseTransform(window, result);
			objectToWindow.inverseTransform(result, result);
		} catch (NoninvertibleTransformException e) {
			Application.handleException(e);
		}

		return result;
	}

	/**
	 * Translates window delta coordinates to object delta coordinates. Typical
	 * delta coordinates are object sizes.
	 * 
	 * @param windowDelta
	 *            window delta coordinates
	 * @return windowDelta in object delta coordinates
	 */
	private Point2D windowToObjDelta(Point windowDelta) {
		Point2D result = new Point2D.Double();

		try {
			AffineTransform temp = new AffineTransform(objectToWindow);
			temp.preConcatenate(currentDisplay);
			temp.invert();
			temp.deltaTransform(windowDelta, result);

		} catch (NoninvertibleTransformException e) {
			Application.handleException(e);
		}

		return result;
	}

	/**
	 * Translates window coordinates to object coordinates based on only the
	 * objectToWindow transformation, not the temporary display transformation.
	 * This is necessary for dragging operations to transform only by the part
	 * of the transformation which is currently newly determined.
	 * 
	 * @param windowFixed
	 *            fixed window coordinates
	 * @return window in object coordinates, only of basic transformation, null
	 *         if there is an error with the transformations which should
	 *         theoretically never be the case
	 */
	protected Point2D windowToObjFixed(Point windowFixed) {
		Point2D result = new Point2D.Double();

		try {
			objectToWindow.inverseTransform(windowFixed, result);
		} catch (NoninvertibleTransformException e) {
			Application.handleException(e);
		}

		return result;
	}

	/**
	 * Fully translates object coordinates to window coordinates.
	 * 
	 * @param object
	 *            object coordinates
	 * @return object in window coordinates
	 */
	protected Point objToWindow(Point2D object) {
		Point2D result = new Point2D.Double();

		objectToWindow.transform(object, result);
		currentDisplay.transform(result, result);

		return new Point((int) result.getX(), (int) result.getY());
	}

	/**
	 * Translates object delta coordinates to window delta coordinates. Typical
	 * delta coordinates are object sizes.
	 * 
	 * @param objectDelta
	 *            object delta coordinates
	 * @return object in window delta coordinates
	 */
	protected Point objToWindowDelta(Point2D objectDelta) {
		Point2D result = new Point2D.Double();

		objectToWindow.deltaTransform(objectDelta, result);
		currentDisplay.deltaTransform(result, result);

		return new Point((int) result.getX(), (int) result.getY());
	}

	/**
	 * Translates object coordinates to window coordinates based on only the
	 * objectToWindow transformation, not the temporary display transformation.
	 * This is necessary for dragging operations to transform only by the part
	 * of the transformation which is currently newly determined.
	 * 
	 * @param objectFixed
	 *            fixed object coordinates
	 * @return object in window coordinates, only of basic transformation
	 */
	protected Point objToWindowFixed(Point2D objectFixed) {
		Point2D result = new Point2D.Double();

		objectToWindow.transform(objectFixed, result);

		return new Point((int) result.getX(), (int) result.getY());
	}

	@Override
	public void zoomIn() {
		zoom(new Point(getWidth() / 2, getHeight() / 2), DEFAULT_ZOOM_IN);
	}

	@Override
	public void zoomOut() {
		zoom(new Point(getWidth() / 2, getHeight() / 2), DEFAULT_ZOOM_OUT);
	}

	/**
	 * Zooms (in the objectToWindow transformation).
	 * 
	 * @param winCenter
	 *            center of the zooming operation in window space
	 * @param factor
	 *            zooming value. if < 1 zooms out, if > 1 zooms in
	 */
	public void zoom(Point winCenter, double factor) {
		// translate the center
		Point2D objCenter = windowToObj(winCenter);

		objectToWindow.translate(+objCenter.getX(), +objCenter.getY());
		objectToWindow.scale(factor, factor);
		objectToWindow.translate(-objCenter.getX(), -objCenter.getY());

		arrange(true);
		repaint();
	}

	/**
	 * Zooms temporary (in the current display transformation).
	 * 
	 * @param objCenter
	 *            center of the zooming operation in object space
	 * @param factor
	 *            zooming value. if < 1 zooms out, if > 1 zooms in
	 */
	public void zoomTemp(Point2D objCenter, double factor) {
		AffineTransform objectToWindowInverse = new AffineTransform(
				objectToWindow);
		try {
			objectToWindowInverse.invert();
		} catch (NoninvertibleTransformException e) {
			Application.handleException(e);
		}

		currentDisplay.setToIdentity();

		currentDisplay.concatenate(objectToWindow);

		currentDisplay.translate(+objCenter.getX(), +objCenter.getY());
		currentDisplay.scale(factor, factor);
		currentDisplay.translate(-objCenter.getX(), -objCenter.getY());

		currentDisplay.concatenate(objectToWindowInverse);

		arrange(true);
		repaint();
	}

	@Override
	public void resetView() {
		double zoomFactor = PipelinePanel.DEFAULT_ZOOM
				* (Double) ModelProxy.getInstance().getSettings()
						.getValue(SettingType.DEFAULT_ZOOM_SIZE);
		objectToWindow.setToScale(zoomFactor, zoomFactor);
		arrange(true);
	}

	@Override
	public void showEntireView() {
		if (functions.size() < 1) {
			Application.handleException(new ControlledException(this,
					ExceptionSeverity.WARNING, I18N.getInstance().getString(
							"View.Pipeline.NoFunctionsForEntireView")));
		}

		double left = objTopLeft.getX();
		double top = objTopLeft.getY();
		double right = objBottomRight.getX();
		double bottom = objBottomRight.getY();

		/*
		 * Construct an affine transformation so that (left,top) |-> (0,0) and
		 * (right, bottom) |-> (getWidth(),getHeight())
		 * 
		 * respect the aspect, tho
		 */

		double objAspect = (right - left) / (bottom - top);
		double winAspect = getWidth() / (double) getHeight();
		double zoom = 1.0;
		if (objAspect > winAspect) {
			zoom = getWidth() / (right - left);
		} else {
			zoom = getHeight() / (bottom - top);
		}

		objectToWindow.setToIdentity();
		objectToWindow.scale(zoom, zoom);
		objectToWindow.translate(-left, -top);

		arrange(true);
	}

	/**
	 * <b>Note:</b> All addition and removal of objects must be done here.
	 */
	@Override
	public void update(Observable o, Object arg) {
		// check for notice from the pipeline model
		if (arg instanceof PipelineObserverObject) {
			PipelineObserverObject poo = (PipelineObserverObject) arg;

			switch (poo.getType()) {

			// new function was added
			case ADD_FUNCTION:
				PipelineFunction pfAdd = new PipelineFunction(
						poo.getChangedFunction(), this);
				functions.add(pfAdd);

				layeredPane.add(pfAdd, FUNCTION_LAYER);
				for (PipelineConnector pc : pfAdd.getConnectors()) {
					connectors.put(pc.getModelConnector(), pc);
					layeredPane.add(pc, CONNECTOR_LAYER);

					for (PipelineLink pl : pc.getOutLinks()) {
						layeredPane.add(pl, LINK_LAYER);
					}
				}
				break;

			// properties of a function changed
			case CHANGE_FUNCTION:
				for (PipelineFunction pfChange : functions) {
					if (pfChange.getModelFunction().equals(
							poo.getChangedFunction())) {

						// if this function has links to its in-connectors,
						// arrange those functions too
						for (PipelineConnector pc : pfChange.getConnectors()) {
							if (!pc.isOutpipes()) {
								for (PipelineLink pl : pc.getInLinks()) {
									pl.getLinkSource().arrangeLinks();
								}
							}
						}
					} /* if pfChange.equals(poo) */
				}
				break;

			// a function got removed
			case DELETE_FUNCTION:
				for (int i = 0; i < functions.size(); i++) {
					PipelineFunction pfDelete = functions.get(i);

					if (pfDelete.getModelFunction().equals(
							poo.getChangedFunction())) {

						// clean-up on isle three
						layeredPane.remove(pfDelete);
						for (PipelineConnector pc : pfDelete.getConnectors()) {
							// delete in links
							int j = 0;
							while (j < pc.getInLinks().size()) {
								PipelineLink pl = pc.getInLinks().get(j);
								pl.getLinkSource().removeLinkTo(
										pl.getLinkDestination());
								layeredPane.remove(pl);
								j++;
							}

							// delete out links
							int k = 0;
							while (k < pc.getOutLinks().size()) {
								PipelineLink pl = pc.getOutLinks().get(k);
								pl.getLinkSource().removeLinkTo(
										pl.getLinkDestination());
								layeredPane.remove(pl);
								k++;
							}

							// delete connector
							connectors.remove(pc.getModelConnector());
							layeredPane.remove(pc);
						}

						// deselect stuff if necessary
						if (pfDelete.equals(selected)) {
							selected(null);
						}
						functions.remove(i);
						break;
					}
				}
				break;

			// the whole pipeline was exchanged
			case FULLCHANGE:
				// let's pray for the GC to get this right
				functions.clear();
				connectors.clear();
				layeredPane.removeAll();
				System.gc();

				layeredPane.add(connectionPreview);

				for (AbstractFunction af : ModelProxy.getInstance()
						.getPipeline().getFunctions()) {
					PipelineFunction pfFullChange = new PipelineFunction(af,
							this);

					functions.add(pfFullChange);

					layeredPane.add(pfFullChange, FUNCTION_LAYER);
					for (PipelineConnector pc : pfFullChange.getConnectors()) {
						connectors.put(pc.getModelConnector(), pc);
						layeredPane.add(pc, CONNECTOR_LAYER);
					}
				}

				// start linking when all connectors are truly known
				for (PipelineConnector pc : connectors.values()) {
					pc.generateLinksFromModel();

					for (PipelineLink pl : pc.getOutLinks()) {
						layeredPane.add(pl, LINK_LAYER);
					}
				}

				// deselect stuff
				selected(null);
				break;

			// new connection added
			case ADD_CONNECTION:
				PipelineConnector sourceAdd = findConnector(poo
						.getChangedConnectors()[0]);
				PipelineLink plAdd = sourceAdd.addLinkTo(findConnector(poo
						.getChangedConnectors()[1]));
				layeredPane.add(plAdd, LINK_LAYER);
				break;

			// connection deleted
			case DELETE_CONNECTION:
				PipelineConnector sourceDel = findConnector(poo
						.getChangedConnectors()[0]);
				PipelineLink plDel = sourceDel.removeLinkTo(findConnector(poo
						.getChangedConnectors()[1]));
				if (plDel != null) {
					layeredPane.remove(plDel);
				}
				break;
			}
		}

		arrange(true);
		layeredPane.repaint();

		// this is better reset here
		connectionStart = null;

		// recreate topleft and bottomright
		calculateEdges();
		updateScrollbars();

	}

	/**
	 * Calculates objTopLeft and objBottomRight.
	 */
	private void calculateEdges() {
		if (functions.size() > 0) {
			double left = Double.MAX_VALUE;
			double top = Double.MAX_VALUE;
			double right = Double.MIN_VALUE;
			double bottom = Double.MIN_VALUE;

			for (PipelineFunction pf : functions) {
				double thisX = pf.getModelLocation().getX();
				double thisY = pf.getModelLocation().getY();

				left = Math.min(left, thisX);
				top = Math.min(top, thisY);
				right = Math.max(right, thisX + pf.getPreferredSize().width);
				bottom = Math.max(bottom, thisY + pf.getPreferredSize().height);
			}

			objTopLeft.setLocation(left, top);
			objBottomRight.setLocation(right, bottom);
		} else {
			objTopLeft.setLocation(0, 0);
			objBottomRight.setLocation(0, 0);
		}
	}

	/**
	 * Updates the valid value ranges of the scroll bars.
	 */
	private void updateScrollbars() {
		// current size of the window, not needed to be scrolled
		Point2D winSize = windowToObjDelta(new Point(getWidth(), getHeight()));

		horizontalScroll.setMinimumSilently((int) objTopLeft.getX());
		horizontalScroll.setMaximumSilently((int) Math.max(0.0,
				(objBottomRight.getX() - winSize.getX())));

		verticalScroll.setMinimumSilently((int) objTopLeft.getY());
		verticalScroll.setMaximumSilently((int) Math.max(0.0,
				(objBottomRight.getY() - winSize.getY())));

		Point2D objWindowZero = windowToObj(new Point(0, 0));

		horizontalScroll.setValueSilently((int) objWindowZero.getX());
		verticalScroll.setValueSilently((int) objWindowZero.getY());
	}

	/**
	 * Arranges all the {@link PipelineFunction}s after a move/zoom change
	 * 
	 * @param updateScrolls
	 *            whether the scrollbars should be updated. necessary to prevent
	 *            infinite recursion
	 */
	private void arrange(boolean updateScrolls) {
		for (PipelineFunction pf : functions) {
			arrange(pf);
		}

		// now all connectors are arranged and we can arrange the links
		for (PipelineFunction pf : functions) {
			pf.arrangeLinks();
		}

		calculateEdges();
		if (updateScrolls) {
			updateScrollbars();
		}
	}

	/**
	 * Arrange a specific {@link PipelineFunction} after any change
	 * 
	 * @param pf
	 *            the {@link PipelineFunction} to arrange
	 */
	private void arrange(PipelineFunction pf) {
		Point location = objToWindow(pf.getModelLocation());
		pf.setLocation(location);

		Point size = new Point(pf.getPreferredSize().width,
				pf.getPreferredSize().height);
		size = objToWindowDelta(size);
		pf.setSize(size.x, size.y);

		if (pf.equals(selected)) {
			pf.setBorder(BorderFactory.createLineBorder(Color.BLACK, 2));
		} else if (!pf.getModelFunction().isComplete()) {
			pf.setBorder(BorderFactory.createLineBorder(Color.RED, 2));
		} else {
			pf.setBorder(null);
		}

		pf.arrangeConnectors();
	}

	/**
	 * Forwards hint display from {@link PipelineFunction}s and
	 * {@link LibraryPanel} under the cursor to the {@link InspectorPanel}.
	 * 
	 * @param hintText
	 *            the hint to display
	 */
	public void setHint(String hintText) {
		functionInspector.setHintText(hintText);
	}

	/**
	 * Is called when a {@link LibraryFunction} that canDragAndDrop was dragged
	 * onto the {@link PipelinePanel}
	 * 
	 * @param libraryFunction
	 *            The new function to add
	 * @param at
	 *            the position of the new function, in PipelinePanel's window
	 *            coordinates
	 */
	public void draggedOnto(LibraryFunction libraryFunction, Point at) {

		at = findNextFreePoint(at, null);

		// drag & drop functionality : add function
		Action a = ActionRegistry.getInstance().get(AddFunctionAction.class);
		Point2D newPosition = windowToObj(at);

		ContainingLocationEvent cle = new ContainingLocationEvent(this,
				libraryFunction.getModelFunctionPrototype(), newPosition);
		a.actionPerformed(cle);
	}

	/**
	 * @return the currently selected object ({@link PipelineFunction} or
	 *         {@link PipelineLink})
	 */
	public Object getSelected() {
		return selected;
	}

	/**
	 * Called when a child object thinks it got selected. Only defined for
	 * {@link PipelineFunction} and {@link PipelineLink}.
	 * 
	 * @param childObject
	 *            child function to be selected
	 */
	public void selected(Object childObject) {
		selected = childObject;

		if (selected != null) {
			if (selected instanceof PipelineFunction) {
				repaint();

				// edit in inspector panel
				PipelineFunction pf = (PipelineFunction) childObject;
				functionInspector.inspect(pf.getModelFunction());

			} else if (selected instanceof PipelineLink) {
				repaint();
			} else {
				Application.handleException(new ControlledException(this,
						ExceptionSeverity.UNEXPECTED_BEHAVIOR, I18N
								.getInstance().getString(
										"View.Pipeline.IllegalSelection",
										selected.toString())));
			}
		} else {
			functionInspector.inspect(null);
			repaint();
		}

		arrange(false);

		// enable deleting & duplicating
		ActionRegistry.getInstance().get(DeleteSelectionAction.class)
				.setEnabled(selected != null);
		ActionRegistry
				.getInstance()
				.get(DuplicateFunctionAction.class)
				.setEnabled(
						(selected != null)
								&& (selected instanceof PipelineFunction));
	}

	/**
	 * @param activeTool
	 *            the active {@link Tool} to set
	 * @param newCursor
	 *            the cursor associated with the new tool, or null if no change
	 */
	public void setActiveTool(Tool activeTool, Cursor newCursor) {
		abortConnect();
		this.activeTool = activeTool;
		if (newCursor != null) {
			this.setCursor(newCursor);
		}
	}

	/**
	 * @return the activeTool
	 */
	public Tool getActiveTool() {
		return activeTool;
	}

	/**
	 * Sets the dragging point manually for objects that need to catch the
	 * {@link MouseEvent} to get dragged around. (Typically,
	 * {@link MouseListener}s should be forwarded though. But forwarding mouse
	 * events in this case would cause a deselection of the dragged object)
	 * 
	 * @param winDraggingFrom
	 *            point dragging started from (window space) to set
	 */
	public void setDraggingFrom(Point winDraggingFrom) {
		this.draggingFrom = windowToObj(winDraggingFrom);
	}

	/**
	 * @return the verticalScroll
	 */
	public JScrollBar getVerticalScroll() {
		return verticalScroll;
	}

	/**
	 * @return the horizontalScroll
	 */
	public JScrollBar getHorizontalScroll() {
		return horizontalScroll;
	}

	/**
	 * @param lookFor
	 *            model {@link Connector} to look for
	 * @return the pipeline, i.e. {@link PipelineConnector} associated with that
	 *         model {@link Connector}, null if none is found
	 */
	protected PipelineConnector findConnector(AbstractConnector lookFor) {
		return connectors.get(lookFor);
	}

	/**
	 * Adds the connectionPoint as an end point to the current connection (which
	 * has at most 2 points). This means: First call - start of new connection.
	 * Second call - end of new connection.
	 * 
	 * @param connectionPoint
	 *            point in the connection
	 */
	public void connect(PipelineFunction connectionPoint) {
		if (this.connectionStart == null) {
			this.connectionStart = connectionPoint;
			this.connectionPreview.setSource(connectionPoint);
		} else {
			ConnectingFunctionsEvent cfe = new ConnectingFunctionsEvent(this,
					connectionStart.getModelFunction(),
					connectionPoint.getModelFunction());
			ActionRegistry.getInstance().get(AddConnectionAction.class)
					.actionPerformed(cfe);
			this.connectionStart = null;
		}
	}

	/**
	 * Aborts a current try to connect two functions.
	 */
	public void abortConnect() {
		this.connectionStart = null;
		this.connectionPreview.setVisible(false);
	}

	/**
	 * @return the layeredPane
	 */
	public JLayeredPane getLayeredPane() {
		return layeredPane;
	}

	/**
	 * Finds out whether you can safely place a function here without occluding
	 * another one.
	 * 
	 * @param newPoint
	 *            {@link Point} where the new Function would be situated
	 * @param ignore
	 *            Ignore this function. Useful if you're currently moving this
	 *            one. May be null.
	 * @return true, if the new function would collide with an existing one,
	 *         false otherwise.
	 */
	public boolean wouldCollide(Point newPoint, PipelineFunction ignore) {

		/*
		 * number of pixels to subtract from the technically necessity to have
		 * functions touching, so that the feature is not too restrictive
		 */
		int grace = 0;

		for (PipelineFunction pf : functions) {
			if (pf.equals(ignore)) {
				continue;
			}

			if ((newPoint.x >= pf.getX() - pf.getWidth() + grace)
					&& (newPoint.y >= pf.getY() - pf.getHeight() + grace)
					&& (newPoint.x <= pf.getX() + pf.getWidth() - grace)
					&& (newPoint.y <= pf.getY() + pf.getHeight() - grace)) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Finds the next free & usable position on the pipeline panel, based on a
	 * preferred location.
	 * 
	 * @param at
	 *            location where to find the nearest free point for
	 * @param forFunc
	 *            the function for which the place should be found. May be null
	 * @return the nearest free point in the area of at
	 */
	public Point findNextFreePoint(Point at, PipelineFunction forFunc) {
		Point result = new Point(at);
		double dist = 0.0;

		while (wouldCollide(result, forFunc)) {
			dist += 20.0;

			// angle between x to the right, y downwards in [0, Math.PI / 2]
			for (double angle = 0.0; angle < Math.PI / 2.0; angle += Math.PI / 20.0) {

				result = new Point(at);
				result.translate((int) (+Math.cos(angle) * dist),
						(int) (+Math.sin(angle) * dist));

				if (!wouldCollide(result, forFunc)) {
					break;
				}
			} /* for */
		}

		return result;
	}

}
